#include <asm/io.h>
#include <asm/regs.h>
#include <asm/system.h>
#include <cnix/driver.h>

#define NAVL 0x00 /* N/A */

#define MODE3	0
#define MODE12	1
#define MODE13	2
#define MODEX	3

typedef struct{
	unsigned short ctrl;	/* used to fill index */
	unsigned char index;
	unsigned short read;	/* used to read data */
	unsigned short write;	/* used to write data */
	unsigned char modes[4];
	unsigned char save;
}vga_reg_value_pair_t;

/* these values come from http://wiki.osdev.org */
static vga_reg_value_pair_t vga_reg_values[] =
{
	// Mode Control
	{ 0x3C0, 0x10, 0x3C1, 0x3C0, { 0x0C, 0x01, 0x41, 0x41 }, 0x00 },
	// Overscan Register
	{ 0x3C0, 0x11, 0x3C1, 0x3C0, { 0x00, 0x00, 0x00, 0x00 }, 0x00},
	// Color Plane Enable
	{ 0x3C0, 0x12, 0x3C1, 0x3C0, { 0x0F, 0x0F, 0x0F, 0x0F }, 0x00 },
	// Horizontal Panning
	{ 0x3C0, 0x13, 0x3C1, 0x3C0, { 0x08, 0x00, 0x00, 0x00 }, 0x00 },
	// Color Select
	{ 0x3C0, 0x14, 0x3C1, 0x3C0, { 0x00, 0x00, 0x00, 0x00 }, 0x00 },

	// Miscellaneous Output Register
	{ 0x3C2, NAVL, 0x3CC, 0x3CC, { 0x67, 0xE3, 0x63, 0xE3 }, 0x00 },

	// Clock Mode Register
	{ 0x3C4, 0x01, 0x3C5, 0x3C5, { 0x00, 0x01, 0x01, 0x01 }, 0x00 },
	// Character select
	{ 0x3C4, 0x03, 0x3C5, 0x3C5, { 0x00, 0x00, 0x00, 0x00 }, 0x00 },
	// Memory Mode Register
	{ 0x3C4, 0x04, 0x3C5, 0x3C5, { 0x07, 0x02, 0x0E, 0x06 }, 0x00 },

	// Mode Register
	{ 0x3CE, 0x05, 0x3CF, 0x3CF, { 0x10, 0x00, 0x40, 0x40 }, 0x00 },
	// Miscellaneous Register
	{ 0x3CE, 0x06, 0x3CF, 0x3CF, { 0x0E, 0x05, 0x05, 0x05 }, 0x00 },

	// set the right value for 0x3C2 before operating on 0x3D4 and 0x3DA

	// Horizontal Total
	{ 0x3D4, 0x00, 0x3D5, 0x3D5, { 0x5F, 0x5F, 0x5F, 0x5F }, 0x00 },
	// Horizontal Display Enable End
	{ 0x3D4, 0x01, 0x3D5, 0x3D5, { 0x4F, 0x4F, 0x4F, 0x4F }, 0x00 },
	// Horizontal Blank Start
	{ 0x3D4, 0x02, 0x3D5, 0x3D5, { 0x50, 0x50, 0x50, 0x50 }, 0x00 },
	// Horizontal Blank End
	{ 0x3D4, 0x03, 0x3D5, 0x3D5, { 0x82, 0x82, 0x82, 0x82 }, 0x00 },
	// Horizontal Retrace Start
	{ 0x3D4, 0x04, 0x3D5, 0x3D5, { 0x55, 0x54, 0x54, 0x54 }, 0x00 },
	//Horizontal Retrace End
	{ 0x3D4, 0x05, 0x3D5, 0x3D5, { 0x81, 0x80, 0x80, 0x80 }, 0x00 },
	// Vertical Total
	{ 0x3D4, 0x06, 0x3D5, 0x3D5, { 0xBF, 0x0B, 0xBF, 0x0D }, 0x00 },
	// Overflow Register
	{ 0x3D4, 0x07, 0x3D5, 0x3D5, { 0x1F, 0x3E, 0x1F, 0x3E }, 0x00 },
	// Preset row scan
	{ 0x3D4, 0x08, 0x3D5, 0x3D5, { 0x00, 0x00, 0x00, 0x00 }, 0x00 },
	// Maximum Scan Line
	{ 0x3D4, 0x09, 0x3D5, 0x3D5, { 0x4F, 0x40, 0x41, 0x41 }, 0x00 },
	// Vertical Retrace Start
	{ 0x3D4, 0x10, 0x3D5, 0x3D5, { 0x9C, 0xEA, 0x9C, 0xEA }, 0x00 },
	// Start Address High byte
	{ 0x3D4, 0x0C, 0x3D5, 0x3D5, { 0x00, 0x00, 0x00, 0x00 }, 0x00 },
	// Start Address Low byte
	{ 0x3D4, 0x0D, 0x3D5, 0x3D5, { 0x00, 0x00, 0x00, 0x00 }, 0x00 },
	// Vertical Retrace End
	{ 0x3D4, 0x11, 0x3D5, 0x3D5, { 0x8E, 0x8C, 0x8E, 0xAC }, 0x00 },
	// Vertical Display Enable End
	{ 0x3D4, 0x12, 0x3D5, 0x3D5, { 0x8F, 0xDF, 0x8F, 0xDF }, 0x00 },
	// Logical Width
	{ 0x3D4, 0x13, 0x3D5, 0x3D5, { 0x28, 0x28, 0x28, 0x28 }, 0x00 },
	// Underline Location
	{ 0x3D4, 0x14, 0x3D5, 0x3D5, { 0x1F, 0x00, 0x40, 0x00 }, 0x00 },
	// Vertical Blank Start
	{ 0x3D4, 0x15, 0x3D5, 0x3D5, { 0x96, 0xE7, 0x96, 0xE7 }, 0x00 },
	// Vertical Blank End
	{ 0x3D4, 0x16, 0x3D5, 0x3D5, { 0xB9, 0x04, 0xB9, 0x06 }, 0x00 },
	// Mode Control
	{ 0x3D4, 0x17, 0x3D5, 0x3D5, { 0xA3, 0xE3, 0xA3, 0xE3 }, 0x00 },
};

#define REG_VALUE_PAIR_NUM \
	(sizeof(vga_reg_values) / sizeof(vga_reg_value_pair_t))

static void switch_into_index_state(void)
{
	inb_p(0x3DA);
}

static void vga_save_registers(void)
{
	int i;
	unsigned char val;
	unsigned long flags;

	lock(flags);

	switch_into_index_state();

	for (i = 0; i < REG_VALUE_PAIR_NUM; i++)
	{
		if (vga_reg_values[i].ctrl == 0x3C2)
		{
			break;
		}

		outb_p(vga_reg_values[i].index, vga_reg_values[i].ctrl);
		vga_reg_values[i].save = inb_p(vga_reg_values[i].read);
	}

	val = inb_p(vga_reg_values[i].read);
	vga_reg_values[i].save = val;

	for (i++; i < REG_VALUE_PAIR_NUM; i++)
	{
		if (vga_reg_values[i].ctrl == 0x3D4)
		{
			break;
		}

		outb_p(vga_reg_values[i].index, vga_reg_values[i].ctrl);
		vga_reg_values[i].save = inb_p(vga_reg_values[i].read);
	}

	outb_p(val | 0x01, 0x3C2);

	for(; i < REG_VALUE_PAIR_NUM; i++)
	{
		outb_p(vga_reg_values[i].index, vga_reg_values[i].ctrl);
		vga_reg_values[i].save = inb_p(vga_reg_values[i].read);
	}

	unlock(flags);
}

static void vga_set_mode(int mode)
{
	int i;
	unsigned long flags;

	lock(flags);

	switch_into_index_state();

	for (i = 0; i < REG_VALUE_PAIR_NUM; i++)
	{
		if (vga_reg_values[i].ctrl == 0x3C2)
		{
			outb_p(vga_reg_values[i].modes[mode], 0x3C2);
			continue;
		}

		outb_p(vga_reg_values[i].index, vga_reg_values[i].ctrl);
		outb_p(vga_reg_values[i].modes[mode], vga_reg_values[i].write);
	}

	unlock(flags);
}

static void vga_restore_registers(void)
{
	int i;
	unsigned long flags;
	unsigned char val = 0x00;

	lock(flags);

	switch_into_index_state();

	for (i = 0; i < REG_VALUE_PAIR_NUM; i++)
	{
		if (vga_reg_values[i].ctrl == 0x3C2)
		{
			val = vga_reg_values[i].save;
			break;
		}

		outb_p(vga_reg_values[i].index, vga_reg_values[i].ctrl);
		outb_p(vga_reg_values[i].save, vga_reg_values[i].write);
	}

	for (i++; i < REG_VALUE_PAIR_NUM; i++)
	{
		if (vga_reg_values[i].ctrl == 0x3D4)
		{
			break;
		}

		outb_p(vga_reg_values[i].index, vga_reg_values[i].ctrl);
		outb_p(vga_reg_values[i].save, vga_reg_values[i].write);
	}

	outb_p(val | 0x01, 0x3C2);

	for (; i < REG_VALUE_PAIR_NUM; i++)
	{
		outb_p(vga_reg_values[i].index, vga_reg_values[i].ctrl);
		outb_p(vga_reg_values[i].save, vga_reg_values[i].write);
	}

	outb_p(val, 0x3C2);

	unlock(flags);
}

#define vga_select_page(page) outb_p(page, 0x3CF)

#define VGA_ADDR 0xA0000

static unsigned char * videobuf = (unsigned char *)VGA_ADDR;

static void vga_mode12_putpixel(int x, int y, unsigned char c)
{
	int m, n;
	unsigned char * byte;

	m = (x + 640 * y) / 8;
	n = (~x) & 0x07;

	byte = videobuf + m;

	vga_select_page(0);
	*byte = (*byte & ~(0x01 << n)) | ((c & 0x01) << n);

	vga_select_page(1);
	*byte = (*byte & ~(0x01 << n)) | (((c & 0x02) >> 1) << n);

	vga_select_page(2);
	*byte = (*byte & ~(0x01 << n)) | (((c & 0x04) >> 2) << n);

	vga_select_page(3);
	*byte = (*byte & ~(0x01 << n)) | (((c & 0x08) >> 3) << n);
}

static unsigned char vga_mode12_getpixel(int x, int y)
{
	return 0x00;
}

static void vga_mode12_clear_scr(void)
{
	int i;

	outb_p(0x04, 0x3CE);

	vga_select_page(0);

	for (i = 0; i < (640 * 480) / 8; i++)
	{
		*(videobuf + i) = 0x00;
	}

	vga_select_page(1);

	for (i = 0; i < (640 * 480) / 8; i++)
	{
		*(videobuf + i) = 0x00;
	}

	vga_select_page(2);

	for (i = 0; i < (640 * 480) / 8; i++)
	{
		*(videobuf + i) = 0x00;
	}

	vga_select_page(3);

	for (i = 0; i < (640 * 480) / 8; i++)
	{
		*(videobuf + i) = 0x00;
	}
}

static void vga_mode12_draw_rectangle(
	int x1, int y1, int x2, int y2, unsigned char color
	)
{
	int i;

	for (i = x1; i < x2 + 1; i++)
	{
		vga_mode12_putpixel(i, y1, color);
	}

	for (i = x1; i < x2 + 1; i++)
	{
		vga_mode12_putpixel(i, y2, color);
	}

	for (i = y1; i < y2 + 1; i++)
	{
		vga_mode12_putpixel(x1, i, color);
	}

	for (i = y1; i < y2 + 1; i++)
	{
		vga_mode12_putpixel(x2, i, color);
	}
}

#include <cnix/modconfig.h>

#ifdef VGA_MODULE

int module_enter(void)
{
	vga_save_registers();
	vga_set_mode(MODE12);

	vga_mode12_clear_scr();

	vga_mode12_draw_rectangle(10, 10, 200, 200, 0x09);

	return 1;
}

int module_exit(void)
{
	vga_restore_registers();

	return 0;
}

#endif
